package Batch::Cluster;

use Moose;
use Batch::Cluster::Result;
use GRID::Machine;
use IO::Select;

our $VERSION = '0.02';

# Atributo que contendrá la lista de máquinas remotas
has 'machines' =>
(
	traits => ['Array'],
	is => 'rw',
	isa => 'ArrayRef[Any]',
	required => 1,
	default => sub { [] },
	handles =>
	{
    	all_machines => 'elements',	
    	add_machine  => 'push',
    	num_machine  => 'count',
    	get_machine  => 'shift',
	}
);

# Sobrecarga del constructor
around BUILDARGS => sub 
{
	my $orig = shift;
	my $class = shift;	

	my @machines;
	my @hosts;
	
	# Podemos recibir una referencia a una lista con los nombres de los hosts remotos
	if(@_ == 1 and ref $_[0] eq 'ARRAY')
	{
		@hosts = @{$_[0]};
	}
	# O bien, todos los nombres de los hosts remotos por separado
	else
	{
		@hosts = @_;
	}
	# Para cada nombre, creamos el objeto GRID::Machine
	for(@hosts)
	{
		die "Se esperaba el nombre de una máquina remota \n" unless (ref eq '');
		my $m;
		eval
		{
			$m = GRID::Machine->new( host => $_ );
		};
		if($@)
		{
			warn "Warning: No se pudo conectar con $_, debido a $@ \n";
		}
		else
		{
			push @machines, $m;	
		}
		
	}
		
	return $class->$orig('machines' => \@machines);
};

# Esta subrutina es llamada luego de llamar al constructor.
# Comprueba que se ha podido conectar con al menos una máquina remota.
sub BUILD 
{
	my $self = shift;
	die 'No se pudo conectar con ninguna máquina remota' unless $self->num_machine();
}
  
# Método que hace las ejecuciones de los comandos en las máquinas remotas
# usando un objeto IO::Select para leer las salidas de los procesos remotos
sub run
{
	my $self = shift;
	
	# Añadimos un pipe al final de cada comando para que nos devuelva su salida
	my $i = 0;
	my @commands = map { [ "$_ | ", $i++ ] } @_;
	
	my $num_commands = @commands;
	my @machines = $self->all_machines();
	my $socket = IO::Select->new();
	
	# En %map, tendremos el handler del proceso remoto, asociado con el objeto
	# GRID::Machine que gestiona la máquina en donde se ejecuta y además, 
	# el índice del comando ejecutado en esa máquina
	my %map;
	# Obtenemos el mínimo entre el número de comandos y de máquinas
	my $minimo = @commands; 
	$minimo = @machines if @machines < $minimo;
	
	# Asignamos las primeras tareas a las máquinas.
	for(0 .. $minimo - 1)
	{
		my $machine = $machines[$_];
		my ($command, $num_command) = @{ shift @commands };	
		my $handler = $machine->open($command);
		$map{scalar $handler} = [ $machine, $num_command ];		
		$socket->add($handler); 
	}
	
	my $result = Batch::Cluster::Result->new();
	my @ready;
	my $count = 0;
	# Vamos leyendo las salidas de los procesos remotos mientras 
	# estén disponibles. Si quedasen algunas tareas pendiente, se 
	# le asigna a la primera máquina que quede libre
	do 
	{
		push @ready, $socket->can_read unless @ready;
		my $handle = shift @ready;
		
		my $res;
		my $temp_buffer;
		my $readed = 1;
		while ($readed != 0 and defined $readed)
		{
			$readed = sysread($handle, $temp_buffer, 1024);
			$res .= $temp_buffer if($readed != 0 and defined $readed);
		}
		
		chomp $res;
		
		$socket->remove($handle) if eof($handle);
		
		$result->add_result_in_order(GRID::Machine::Result->new( results => [ $res ] ), $map{scalar $handle}[1]);
		
		if(@commands)
		{
			my $machine = $map{scalar $handle}[0];
			delete $map{scalar $handle};
			my ($command, $num_command) = @{ shift @commands };
			$handle = $machine->open($command);
			$socket->add($handle);
			$map{scalar $handle} = [ $machine, $num_command ];
		}
	}
	until (++$count == $num_commands);
	
	# Si el método es llamado en un contexto de lista,
	# devolvemos una lista con los resultados
	if (wantarray) 
	{
		my @results;
		my $i = 0;
    	for ($result->all_results()) 
    	{
      		$results[$i++] = $_->result();
    	}
    	return @results;
	}
	# Sino, el objeto Batch::Cluster::Result
	$result;	
}

# Ejecuta un copyandmake en cada máquina
sub copyandmake
{
	my $self = shift;
	my %args = @_;
	
	die "Se esperaba una referencia a una lista en el parámetro 'files'" unless ref $args{files} eq 'ARRAY';
	$_->copyandmake(%args) for($self->all_machines());
}

# Ejecuta un chdir en cada máquina
sub chdir
{
	my ($self, $dir) = @_;
	my $result = Batch::Cluster::Result->new();
	$result->add_result($_->chdir($dir)) for($self->all_machines());
	return $result;
}


__PACKAGE__->meta->make_immutable();

no Moose;

1;

__END__

=head1 NAME

Batch::Cluster - Módulo para la ejecución de programas en máquinas remotas a través de una conexión ssh

=head1 SYNOPSIS
	
	use Batch::Cluster;
	use List::Util qw{ sum };

	my $np = 20;    
	my $N = 1000;   
	my $clean = 1;  
	
	my @remote_hosts = qw{alu3713@binot alu3713@banot alu3713@benot};
	
	my $c = Batch::Cluster->new(\@remote_hosts) || die "Error Intializing Cluster";
	# Transference of files to remote hosts
	$c->copyandmake
	(
		dir => 'pi',
		makeargs => 'pi',
		files => [ qw{pi.c Makefile} ],
		cleanfiles => $clean,
		cleandirs => $clean, # remove the whole directory at the end
		keepdir => 1,
	);
	
	
	$c->chdir("pi/")  || die "Can't change to pi/\n";
	
	my @commands = map {  "./pi $_ $N $np" } 0..$np-1;
	
	my $pi = sum $c->run(@commands);
	
=head1 DESCRIPTION

Este módulo permite ejecutar programas en máquinas remotas a través de una conexión ssh.
Es una ampliación de GRID::Machine, ya que la misma clase gestiona las comunicaciones
con todas las máquinas remotas a la ves. Permite ejecutar tantos programas como se quiera
en todas las máquinas con las que pueda establecer una conexión ssh.


=head1 AUTHOR

Nicolás Pernas Maradei, E<lt>nicopernas@gmail.comE<gt>
Francisco José Cabrera Hernández, E<lt>alu3651@etsii.ull.esE<gt>
Romén Rodríguez Gil, E<lt>alu3718@etsii.ull.esE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2011 by Nicolás Pernas Maradei, Francisco José Cabrera Hernández, Romén Rodríguez Gil

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.9 or,
at your option, any later version of Perl 5 you may have available.


=cut
